/*
 * Copyright 2021-2022 the original author(https://github.com/wj596)
 * 
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */
package org.jsets.fastboot.operlog.aspectj;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.AdviceSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.jsets.fastboot.concurrent.AsyncActuator;
import org.jsets.fastboot.security.IAuthenticatedUserProvider;
import org.jsets.fastboot.util.StringUtils;
import org.jsets.fastboot.util.WebUtils;
import org.jsets.fastboot.operlog.IOperLogDest;
import org.jsets.fastboot.operlog.OperLogContext;
import org.jsets.fastboot.operlog.annotation.OperLog;
import org.jsets.fastboot.operlog.model.OperInfo;
import org.jsets.fastboot.operlog.model.OperType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 操作日志切面
 *
 * @author wangjie (https://github.com/wj596)
 * @date 2021.07.10 23:56
 */
@Aspect
public class OperLogAspect {

	private static final Logger log = LoggerFactory.getLogger(OperLogAspect.class);

	private final ExpressionParser expressionParser = new SpelExpressionParser();
	private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

	private final IOperLogDest dest;
	private final IAuthenticatedUserProvider authenticatedUserProvider;

	public OperLogAspect(IOperLogDest dest, IAuthenticatedUserProvider authenticatedUserProvider) {
		this.dest = dest;
		this.authenticatedUserProvider = authenticatedUserProvider;
	}

	// 配置织入点
	@Pointcut("@annotation(org.jsets.fastboot.operlog.annotation.OperLog)")
	public void operLogAnnotatedMethod() {
	}

	/**
	 * 处理完请求后执行
	 *
	 * @param joinPoint 切点
	 */
	@AfterReturning(pointcut = "operLogAnnotatedMethod()", returning = "returnValue")
	public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {
		OperInfo operInfo = new OperInfo();
		operInfo.setSuccess(true);
		handleLog(joinPoint, operInfo, returnValue);
	}

	/**
	 * 拦截异常操作
	 * 
	 * @param joinPoint 切点
	 * @param err       异常
	 */
	@AfterThrowing(value = "operLogAnnotatedMethod()", throwing = "err")
	public void doAfterThrowing(JoinPoint joinPoint, Exception err) {
		OperInfo operInfo = new OperInfo();
		operInfo.setSuccess(false);
		operInfo.setErrorMsg(err.getMessage());
		handleLog(joinPoint, operInfo, null);
	}

	/**
	 * @param joinPoint
	 * @param operInfo
	 * @param returnValue
	 */
	protected void handleLog(final JoinPoint joinPoint, final OperInfo operInfo, final Object returnValue) {
		try {
			Method m = getInvokeMethod(joinPoint);
			if (m == null) {
				throw new IllegalArgumentException("invoke method is null");
			}

			Annotation annotation = m.getAnnotation(OperLog.class);
			if (annotation == null) {
				Object miThis = joinPoint.getThis();
				annotation = miThis != null ? miThis.getClass().getAnnotation(OperLog.class) : null;
			}
			if (annotation == null || !(annotation instanceof OperLog)) {
				throw new IllegalStateException("annotation type mismatching");
			}
			OperLog ann = (OperLog) annotation;
			operInfo.setOperTime(new Date());
			operInfo.setSubsystem(ann.subsystem());
			operInfo.setModule(ann.module());
			operInfo.setFunc(ann.func());

			if (null != this.authenticatedUserProvider) {
				try {
					operInfo.setOperUsername(this.authenticatedUserProvider.getCurrentUsernameIfLogined());
				} catch (Exception e) {
					// 忽略
				}
			}

			if (ann.saveParameters() && Objects.nonNull(joinPoint.getArgs()) && joinPoint.getArgs().length > 0) {
				Object[] parameters = new Object[joinPoint.getArgs().length];
				for (int i = 0; i < joinPoint.getArgs().length; i++) {
					Object param = joinPoint.getArgs()[i];
					parameters[i] = param;
				}
				operInfo.setParameters(parameters);
			}
			if (ann.saveReturnValue() && operInfo.getSuccess() && Objects.nonNull(returnValue)) {
				operInfo.setReturnValue(returnValue);
			}
			String className = joinPoint.getTarget().getClass().getName();
			String methodName = joinPoint.getSignature().getName();
			operInfo.setMethodName(className + "." + methodName + "()");
			if (ann.saveRequestInfo()) {
				RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
				if (Objects.nonNull(attributes)) {
					ServletRequestAttributes servletAttributes = (ServletRequestAttributes) attributes;
					HttpServletRequest request = servletAttributes.getRequest();
					if (Objects.nonNull(request)) {
						operInfo.setRequestIp(WebUtils.getClientIp(request));
						operInfo.setRequestMethod(request.getMethod());
					}
				}
			}

			if (ann.operType() == OperType.AUTO) {
				if (methodName.startsWith("insert") || methodName.startsWith("add")) {
					operInfo.setOperType(OperType.INSERT);
				} else if (methodName.startsWith("delete") || methodName.startsWith("del")
						|| methodName.startsWith("remove") || methodName.startsWith("cascadeDelete")) {
					operInfo.setOperType(OperType.DELETE);
				} else if (methodName.startsWith("update") || methodName.startsWith("mod")
						|| methodName.startsWith("modify")) {
					operInfo.setOperType(OperType.UPDATE);
				} else {
					operInfo.setOperType(OperType.OTHER);
				}
			} else {
				operInfo.setOperType(ann.operType());
			}

			if (!OperLogContext.isEmpty()) {
				Map<String, Object> ctxFields = new HashMap<String, Object>();
				for (String key : OperLogContext.getKeys()) {
					Object v = OperLogContext.get(key);
					ctxFields.put(key, v);
				}
				operInfo.setCtx(ctxFields);
			}

			if (StringUtils.notEmpty(ann.content())) {
				if (ann.content().contains("#{")) {
					EvaluationContext context = new StandardEvaluationContext();
					String[] parameterNames = parameterNameDiscoverer.getParameterNames(m);
					if (parameterNames != null) {
						for (int i = 0; i < parameterNames.length; i++) {
							context.setVariable(parameterNames[i], joinPoint.getArgs()[i]);
						}
					}
					if (Objects.nonNull(returnValue)) {
						context.setVariable("return", returnValue);
					}
					if (!OperLogContext.isEmpty()) {
						for (String key : OperLogContext.getKeys()) {
							Object v = OperLogContext.get(key);
							context.setVariable(key, v);
						}
					}
					Expression exp = this.expressionParser.parseExpression(ann.content(), new TemplateParserContext());
					String value = exp.getValue(context).toString();
					operInfo.setContent(value);
				} else {
					operInfo.setContent(ann.content());
				}
			}

			if (ann.async()) {
				AsyncActuator.execute(new Runnable() {
					@Override
					public void run() {
						try {
							dest.pushOperLog(operInfo);
						} catch (Exception e) {
							log.error(e.getMessage(), e);
						}
					}
				});
			} else {
				this.dest.pushOperLog(operInfo);
			}
		} catch (Exception exp) {
			log.error(exp.getMessage(), exp);
		} finally {
			OperLogContext.cleanup();
		}
	}

	protected Method getInvokeMethod(JoinPoint joinPoint) {
		if (joinPoint.getSignature() instanceof MethodSignature) {
			return ((MethodSignature) joinPoint.getSignature()).getMethod();
		} else if (joinPoint.getSignature() instanceof AdviceSignature) {
			return ((AdviceSignature) joinPoint.getSignature()).getAdvice();
		} else {
			throw new IllegalArgumentException("The joint point signature is invalid: expected a MethodSignature or an AdviceSignature but was "+ joinPoint.getSignature());
		}
	}
}
